﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using Search;
using Security;
using Stats;
using Stats.Objects;
using System.Transactions;
using Storage;
using Storage.EntityModels;

namespace Groupware
{
    public class TripManager
    {
        #region SearchTrip

        /// <summary>
        /// Search a new trip (Temporary)
        /// </summary>
        /// <returns>List of trips (Groupware Obj)</returns>
        public static IEnumerable<Trip> TripSearch(
            TripParameter start,
            TripParameter end,
            List<TripFieldParameter> parameters,
            List<UserParameter> driverParameters)
        {
            List<Trip> trips =
                SearchManager.TripSearch(start, end, parameters,driverParameters)
                .ToList()
                .ConvertAll(t => new Trip(t));
            return trips;
        }

        public static IEnumerable<Trip> TripSearch(
            TripParameter start,
            TripParameter end,
            List<TripFieldParameter> parameters,
            List<UserParameter> driverParameters,
            int startIndex,
            int maxResultSize)
        {
            List<Trip> trips =
                SearchManager.TripSearch(start, end, parameters, driverParameters, startIndex, maxResultSize)
                .ToList()
                .ConvertAll(t => new Trip(t));
            return trips;
        }

        public static Trip GetTripById(int tripId)
        {
            Storage.EntityModels.Trip trip;
            try
            { 
                trip = StorageManager.Instance.GetTripById(tripId);
            }
            catch (Storage.Exceptions.TripNotFoundException ex)
            {
                return null;
            }
            return new Trip(trip);
        }

        public static Storage.EntityModels.Trip GetDbTripById(int tripId)
        {
            return StorageManager.Instance.GetTripById(tripId);
        }

        /// <summary>
        /// Retrieves all draft trips created by the specified user
        /// </summary>
        /// <param name="user">the user for which retrieve trips</param>
        /// <returns>The draft trip list</returns>
        public static List<Trip> GetDraftTrips(TripLoaUser user)
        {
            return GetTrips(user, true);
        }

        /// <summary>
        /// Retrieves all published trips, either accomplished or not, created by the specified user
        /// </summary>
        /// <param name="user">the user for which retrieve trips</param>
        /// <returns>The published trip list</returns>
        public static List<Trip> GetAllPublishedTrips(TripLoaUser user)
        {
            return GetTrips(user, false);
        }

        /// <summary>
        /// Retrieves all scheduled trips created by the specified user
        /// </summary>
        /// <param name="user">the user for which retrieve trips</param>
        /// <returns>The scheduled trip list</returns>
        public static List<Trip> GetScheduledTrips(TripLoaUser user)
        {
            return GetTrips(user, false).Where(t => !t.Accomplished).ToList();
        }

        /// <summary>
        /// Retrieves all accomplished trips created by the specified user
        /// </summary>
        /// <param name="user">the user for which retrieve trips</param>
        /// <returns>The accomplished trip list</returns>
        public static List<Trip> GetAccomplishedTrips(TripLoaUser user)
        {
            return GetTrips(user, false).Where(t => t.Accomplished).ToList();
        }

        /// <summary>
        /// Retrieves all trips which the specified user joined in (created trips are not included)
        /// </summary>
        /// <param name="user">the user</param>
        /// <returns>The trip list</returns>
        public static List<Trip> GetAllJoinedTrips(TripLoaUser user)
        {
            if (user == null)
                throw new ArgumentNullException("user");
            List<Trip> trips = (
                from Storage.EntityModels.Users_Stages us in StorageManager.Instance.GetUserById(user.UserID).Users_Stages
                group us by us.Stage.Trip into g
                select new
                {
                    JoinedTrip = g.Key
                }).ToList().ConvertAll(el => new Trip(el.JoinedTrip));
            return trips.Where(t => !t.Draft).ToList();
        }

        private static List<Trip> GetTrips(TripLoaUser user, bool drafts)
        {
            if (user == null)
                throw new ArgumentNullException("user");
            List<Trip> trips = (
                from Storage.EntityModels.Trip t in StorageManager.Instance.GetUserById(user.UserID).Trips
                where t.Draft == drafts
                select t)
                .ToList()
                .ConvertAll(t => new Trip(t));
            return trips;
        }

        /// <summary>
        /// Returns all trips with a request of a seat reservation for a certain user.
        /// </summary>
        /// <param name="userID">The user's Id.</param>
        /// <returns>A List of Trip.</returns>
        public static List<Trip> GetTripsWithReservationRequestByUserId(int userID)
        {
            return StorageManager.Instance
                .GetTripsWithReservationRequestByUserId(userID)
                .ConvertAll(t => new Trip(t));
        }

        public static List<Trip> GetAllTripByUserID(int userID)
        {
            List<Trip> trips =
                StorageManager.Instance.GetTripListByOwnerId(userID)
                .ConvertAll(tid => GetTripById(tid));
            return trips;
        }

        public static Trip GetTripByEventID(int eventID)
        {
            IQueryable<int> uvd = from trip_ev in StorageManager.Instance.Entities.Trips_Events
                                  where trip_ev.EventId == eventID
                                  select trip_ev.TripId;
            return GetTripById(uvd.First());
        }

        #endregion

        #region DeleteTrip

        /// <summary>
        /// Deletes the Trip and ALL related rows from database
        /// Resets passengers privacy levels
        /// It's not allowed to remove an already accomplished Trip
        /// </summary>
        /// <param name="tripId">id of the trip</param>
        /// <param name="idUser">owner's id of the trip</param>
        /// <returns>true if operation succeded, false otherwise</returns>
        /// <remarks>this method does not yet remove events</remarks>
        public static bool deleteTrip(int tripId, int idUser)
        {
            using (TransactionScope scope = new TransactionScope())
            {
                Storage.EntityModels.Trip dbTrip = null;
                
                try {
                    dbTrip = StorageManager.Instance.GetTripById(tripId);
                } catch (Storage.Exceptions.TripNotFoundException) {
                    scope.Complete();
                    return false; 
                }

                if (idUser != dbTrip.OwnerId) { scope.Complete(); return false; }
                if (dbTrip.Accomplished == true) { scope.Complete(); return false; }

                TripLoaDataContext db = StorageManager.Instance.Entities;

                // reset passengers privacy level
                ResetPassengerPrivacyLevel(dbTrip);

                // send a notification message to passengers and to users waiting for reservation
                sendDeleteTripNotification(dbTrip);

                // first, delete all Stages related to this trip
                List<Storage.EntityModels.Stage> stages = StorageManager.Instance.GetAllStagesByTripId(tripId);
                StorageManager.Instance.Entities.Stages.DeleteAllOnSubmit(stages);

                // delete all User_Stages and DrivingInfo
                foreach (Storage.EntityModels.Stage s in stages) 
                {
                    var userStages = from Users_Stages us in db.Users_Stages
                                     where us.StageId == s.Id
                                     select us;

                    var drvgInfo   = from DrivingInfo d in db.DrivingInfos
                                     where d.StageId == s.Id
                                     select d;
                    
                    db.Users_Stages.DeleteAllOnSubmit(userStages);
                    db.DrivingInfos.DeleteAllOnSubmit(drvgInfo);
                }

                // remove all reservation requests made for this trip
                var resRequests = from Storage.EntityModels.ReservationRequest rr in db.ReservationRequests
                                  where rr.TripId == dbTrip.Id
                                  select rr;
                db.ReservationRequests.DeleteAllOnSubmit(resRequests);

                // delete all frequency information related to this trip
                var freqTrip = from FrequencyDetails f in db.FrequencyDetails
                               where f.TripId == dbTrip.Id
                               select f;

                if (freqTrip.Any())
                {
                    var q = from Frequency_DailyFrequency f in db.Frequency_DailyFrequencies
                            where f.FrequencyId == freqTrip.First().Id
                            select f;

                    foreach (Frequency_DailyFrequency f in q)
                    {
                        var qDetails = (from DailyFrequencyDetails dd in db.DailyFrequencyDetails
                                        where dd.Id == f.DailyId
                                        select dd).First();
                        db.DailyFrequencyDetails.DeleteOnSubmit(qDetails);
                    }
                    db.Frequency_DailyFrequencies.DeleteAllOnSubmit(q);
                    db.FrequencyDetails.DeleteAllOnSubmit(freqTrip);
                }

                // delete all notifications with message containing tripId
                var msgs =
                        from Storage.EntityModels.Message m in db.Messages
                        where m.TripId == dbTrip.Id
                        select m;

                foreach (Storage.EntityModels.Message msg in msgs)
                {
                    var notifications =
                        from Storage.EntityModels.Notifications n in db.Notifications
                        where n.MessageId == msg.Id
                        select n;

                    db.Notifications.DeleteAllOnSubmit(notifications);
                    db.Messages.DeleteOnSubmit(msg);
                }

                // Remove Pending feedbacks for this trip. 
                var pFeedbacks = from PendingFeedbacks pf in db.PendingFeedbacks
                                 where pf.TripId == dbTrip.Id
                                 select pf;

                db.PendingFeedbacks.DeleteAllOnSubmit(pFeedbacks);

                // Remove Driver and Passenger feedbacks 
                // WARNING! this operation may be useless because feedbacks could not exist until dbTrip.Accomplished == true !!
                var dFeedbacks = from DriverFeedback df in db.DriverFeedbacks
                                 where df.TripId == dbTrip.Id
                                 select df;

                var pasFeedbacks = from PassengerFeedback pf in db.PassengerFeedbacks
                                   where pf.TripId == dbTrip.Id
                                   select pf;
                db.DriverFeedbacks.DeleteAllOnSubmit(dFeedbacks);
                db.PassengerFeedbacks.DeleteAllOnSubmit(pasFeedbacks);

                // delete trip
                db.Trips.DeleteOnSubmit(dbTrip);
                db.SubmitChanges();
                scope.Complete();
                return true;
            }
        }

        internal static void sendDeleteTripNotification(Storage.EntityModels.Trip dbTrip)
        {
            var stages =  (from Storage.EntityModels.Stage s in StorageManager.Instance.Entities.Stages
                           where s.TripId == dbTrip.Id
                           orderby s.Ordinal
                           select s).ToList();
            
            Storage.EntityModels.Place origin = (Storage.EntityModels.Place) stages.First().Origin;
            Storage.EntityModels.Place destination = (Storage.EntityModels.Place) stages.Last().Destination;

            List<int> users = new List<int>();
            foreach (Storage.EntityModels.Stage s in dbTrip.Stages)
                users.AddRange(StorageManager.Instance.GetUserListByStagePk(s.Id));

            //users.AddRange(StorageManager.Instance.GetUsersWithReservationRequestByTripId(dbTrip.Id)); 
            //only PENDING RES
            var usersWithPendingRes =  from Storage.EntityModels.ReservationRequest r
                                          in StorageManager.Instance.Entities.ReservationRequests
                                       where r.TripId == dbTrip.Id && r.Status == (byte)ReservationRequest.State.PENDING
                                       select r.UserId;
            users.AddRange(usersWithPendingRes);

            Notification n = new Notification()
            {
                TemplateType = Notification.TRIP_DELETED,
                SenderID = dbTrip.OwnerId
            };
            n.AddField("orig_city", origin.City);
            n.AddField("orig_state", origin.State);
            n.AddField("orig_prov", origin.Province);
            n.AddField("dest_city", destination.City);
            n.AddField("dest_prov", destination.Province);
            n.AddField("dest_state", destination.State);
            n.AddField("date", dbTrip.DepartureDate);

            n.Store(users.Distinct().ToList());
        }

        #endregion

        #region Users information

        public static List<Profile> GetAssociatedUsers(int tripID, String uNameLogged)
        {
            var userassociated = new List<Profile>();
            Storage.EntityModels.Trip trip = StorageManager.Instance.GetTripById(tripID);
            //trip.Stages.First().Users_Stages.First
            TripLoaUser uLogged = TripLoaUserManager.Instance.getUser(uNameLogged);
            foreach (Storage.EntityModels.Stage s in trip.Stages)
            {
                foreach (Users_Stages us in s.Users_Stages)
                {
                    Profile userassociated_profile = uLogged.GetProfile(us.UserId);
                    if (!(userassociated.Contains(userassociated_profile)))
                    {
                        userassociated.Add(userassociated_profile);
                    }
                    //a_user = null;
                }
            }
            return userassociated;
        }

        public static List<Profile> GetAssociatedUsers(int tripID, TripLoaUser uLogged)
        {
            return GetAssociatedUsers(tripID, uLogged.UserName);
        }

        //DEPRECATED!!!!!!
        public static List<Profile> GetReservatedUsers(int tripID)
        {
            return new List<Profile>();
        }

        #endregion

        /// <summary>
        /// This method is called from GUI to retrieve the total amount of emissions saved using TripLOA. It calls the Stats Module.
        /// </summary>
        /// <returns>the amount of co2 emissions saved using TripLOA (quintals of Co2)</returns>
        public static double getTripLOAEmissionsSaved()
        {
            return StatsModule.getTotalEmissionsSaved();
        }


        /// <summary>
        /// This method is called from GUI to retrieve the last 10 created trips. It calls the Stats Module.
        /// </summary>
        /// <returns></returns>
        public static List<TripProperties> getLastTenCreatedTrips()
        {
            return StatsModule.getLastNCreatedTrips(10);
        }

        /// <summary>
        /// This method is called from GUI and it gets the total count of non draft created trips. It calls the Stats Module.
        /// </summary>
        /// <returns>count of non draft created trips</returns>
        public static int getTotalCreatedTripsCount()
        {
            return StatsModule.getCreatedTripsCount();
        }

        /// <summary>
        /// Reset the privacy levels between owner and passengers
        /// </summary>
        /// <param name="trip">The Trip to reset</param>
        public static void ResetPassengerPrivacyLevel(Storage.EntityModels.Trip trip)
        {
            // FIX: better impl.
            if (trip.OwnerId == null) throw new NullReferenceException("In questo trip manca l'id di chi l'ha creato! Ma sei di fuori?");
            int idUser = (int)trip.OwnerId;
            for (int i = 0; i < trip.Stages.Count; i++)
            {
                List<int> tmp = StorageManager.Instance.GetUserListByStagePk(trip.Stages[i].Id);
                foreach (int passenger in tmp)
                {
                    if (idUser != passenger &&
                        StorageManager.Instance.GetRelation(idUser, passenger) == PrivacyLevel.Medium)
                        StorageManager.Instance.SetRelation(idUser, passenger, PrivacyLevel.High);
                }
            }
        }
    }

    /// <summary>
    /// This class is responsible of routine check of accomplished Trips. It will manage the trip when the departureDate expires.
    /// </summary>
    public static class TripUpdater
    {
        private static readonly Dictionary<Trip, Timer> _registeredTrips = new Dictionary<Trip, Timer>();

        #region TripLoaTimer

        /// <summary>
        /// This class is a workaround to make Timer have interval larger than int.MaxValue milliseconds.
        /// </summary>
        class TripLoaTimer : Timer
        {
            private long _timeLeft;

            /// <summary>
            /// Gets or sets the Interval.
            /// </summary>
            public new double Interval
            {
                get
                {
                    return base.Interval;
                }
                set
                {
                    if (value > int.MaxValue)
                    {
                        base.Interval = int.MaxValue;
                        _timeLeft = (long)value - int.MaxValue;

                        // NB: needed otherwise after the first Elapsed event the timer won't fire again!
                        AutoReset = true;
                    }
                    else
                    {
                        if (value > 0)
                        {
                            base.Interval = value;
                            _timeLeft = 0;
                        }

                        AutoReset = false;
                    }
                }
            }

            public new event ElapsedEventHandler Elapsed
            {
                add
                {
                    ElapsedEventHandler handler =
                        (sender, e) =>
                        {
                            updateInterval();
                            value(sender, e);
                        };

                    base.Elapsed += handler;
                }
                remove
                {
                    base.Elapsed -= value;
                }
            }

            private void updateInterval()
            {
                Interval = _timeLeft;
            }
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Adds a <see cref="Trip"/> to the _registeredTrips list with a <see cref="Timer"/> associated.
        /// </summary>
        /// <param name="trip">The trip to be Added</param>
        public static void Track(Trip trip)
        {
            if (trip.Draft)
                throw new ArgumentException("A draft trip cannot be tracked.");

            // NB: tracks the trip only if it isn't a draft one.
            DateTime date = trip.DepartureDate.Value;
            addTrip(trip, generateTimer(date, trip));
        } 

        #endregion

        #region Private methods

        /// <summary>
        /// SONO UN METODO INUTILE VOLUTO DA SPARAGNA.
        /// </summary>
        /// <param name="trip">The Trip to remove.</param>
        /// <returns>True if the removal was successful.</returns>
        private static bool removeTrip(Trip trip)
        {
            return _registeredTrips.Remove(trip);
        }

        /// <summary>
        /// SONO UN METODO INUTILE VOLUTO DA RIGHETTI.
        /// </summary>
        /// <param name="trip">The Trip to add.</param>
        /// <param name="wakeUp">The trip related Timer.</param>
        private static void addTrip(Trip trip, Timer wakeUp)
        {
            _registeredTrips.Add(trip, wakeUp);
        }

        /// <summary>
        /// Generates a new <see cref="Timer"/> which will be executed when the trip is accomplished.
        /// </summary>
        /// <param name="departureDate">The Trip's departure date.</param>
        /// <param name="trip">The Trip to be checked</param>
        /// <returns>The new generated Timer</returns>
        private static Timer generateTimer(DateTime departureDate, Trip trip)
        {
            var generatedTimer = new TripLoaTimer
                                     {
                                         AutoReset = false,
                                         Interval = (departureDate - DateTime.Now).TotalMilliseconds
                                     };                

            generatedTimer.Elapsed += (sender, e) => checkTripDetails(trip);
            generatedTimer.Enabled = true;
                        
            return generatedTimer;
        }

        /// <summary>
        /// Check if the Trip is frequent and updates <see cref="Trip.DepartureDate"/> consisently. 
        /// It also updates <see cref="Timer"/> into _registeredTrips.
        /// </summary>
        /// <param name="trip">The Trip to be checked.</param>
        private static void checkTripDetails(Trip trip)
        {
            if (trip.IsFrequent == null)
                throw new ArgumentException("E ALLA VARIABILE ISFREQUENT CHI CI PENSAAA????!!!!");

            // NB: we can assume that at this point Value contains valid data.
            if (trip.IsFrequent.Value)
            {
                // The trip is a frequent one.
                trip.UpdateDepartureDate();
            }
            else
            {
                trip.Accomplished = true;
                TripManager.ResetPassengerPrivacyLevel(trip.DbTrip);
                removeTrip(trip);
            }
        } 

        #endregion
    }
}